'use strict'

import {readdir, readFile, stat, writeFile} from 'node:fs/promises'
import {spawn} from 'node:child_process'
import path from 'node:path'

import shellescape from 'shell-escape'

import {musicExtensions} from './crawlers.js'
import {getTimeStringsFromSec, parseOptions, promisifyProcess} from './general-util.js'

async function timestamps(files) {
  const tsData = []

  let timestamp = 0
  for (const file of files) {
    const args = [
      '-print_format', 'json',
      '-show_entries', 'stream=codec_name:format',
      '-select_streams', 'a:0',
      '-v', 'quiet',
      file
    ]

    const ffprobe = spawn('ffprobe', args)

    let data = ''
    ffprobe.stdout.on('data', chunk => {
      data += chunk
    })

    await promisifyProcess(ffprobe, false)

    let result
    try {
      result = JSON.parse(data)
    } catch (error) {
      throw new Error(`Failed to parse ffprobe output - cmd: ffprobe ${args.join(' ')}`)
    }

    const duration = parseFloat(result.format.duration)

    tsData.push({
      comment: path.basename(file, path.extname(file)),
      timestamp,
      timestampEnd: (timestamp += duration)
    })
  }

  // Serialize to a nicer format.
  for (const ts of tsData) {
    ts.timestamp = Math.trunc(ts.timestamp * 100) / 100
    ts.timestampEnd = Math.trunc(ts.timestampEnd * 100) / 100
  }

  return tsData
}

async function main() {
  const validFormats = ['txt', 'json']

  let files = []

  const opts = await parseOptions(process.argv.slice(2), {
    'format': {
      type: 'value',
      validate(value) {
        if (validFormats.includes(value)) {
          return true
        } else {
          return `a valid output format (${validFormats.join(', ')})`
        }
      }
    },

    'no-concat-list': {type: 'flag'},
    'concat-list': {type: 'value'},

    'out': {type: 'value'},
    'o': {alias: 'out'},

    [parseOptions.handleDashless]: opt => files.push(opt)
  })

  if (files.length === 0) {
    console.error(`Please provide either a directory (album) or a list of tracks to generate timestamps from.`)
    return 1
  }

  if (!opts.format) {
    opts.format = 'txt'
  }

  let defaultOut = false
  let outFromDirectory
  if (!opts.out) {
    opts.out = `timestamps.${opts.format}`
    defaultOut = true
  }

  const stats = []

  {
    let errored = false
    for (const file of files) {
      try {
        stats.push(await stat(file))
      } catch (error) {
        console.error(`Failed to stat ${file}`)
        errored = true
      }
    }
    if (errored) {
      console.error(`One or more paths provided failed to stat.`)
      console.error(`There are probably permission issues preventing access!`)
      return 1
    }
  }

  if (stats.some(s => !s.isFile() && !s.isDirectory())) {
    console.error(`A path was provided which isn't a file or a directory.`);
    console.error(`This utility doesn't know what to do with that!`);
    return 1
  }

  if (stats.length > 1 && !stats.every(s => s.isFile())) {
    if (stats.some(s => s.isFile())) {
      console.error(`Please don't provide a mix of files and directories.`)
    } else {
      console.error(`Please don't provide more than one directory.`)
    }
    console.error(`This utility is only capable of generating a timestamps file from either one directory (an album) or a list of (audio) files.`)
    return 1
  }

  if (files.length === 1 && stats[0].isDirectory()) {
    const dir = files[0]
    try {
      files = await readdir(dir)
      files = files.filter(f => musicExtensions.includes(path.extname(f).slice(1)))
    } catch (error) {
      console.error(`Failed to read ${dir} as directory.`)
      console.error(error)
      console.error(`Please provide a readable directory or multiple audio files.`)
      return 1
    }
    files = files.map(file => path.join(dir, file))
    if (defaultOut) {
      opts.out = path.join(path.dirname(dir), path.basename(dir) + '.timestamps.' + opts.format)
      outFromDirectory = dir.replace(new RegExp(path.sep + '$'), '')
    }
  } else if (process.argv.length > 3) {
    files = process.argv.slice(2)
  } else {
    console.error(`Please provide an album directory or multiple audio files.`)
    return 1
  }

  let tsData
  try {
    tsData = await timestamps(files)
  } catch (error) {
    console.error(`Ran into a code error while processing timestamps:`)
    console.error(error)
    return 1
  }

  const duration = tsData[tsData.length - 1].timestampEnd

  let tsText
  switch (opts.format) {
    case 'json':
      tsText = JSON.stringify(tsData) + '\n'
      break
    case 'txt':
      tsText = tsData.map(t => `${getTimeStringsFromSec(t.timestamp, duration, true).timeDone} ${t.comment}`).join('\n') + '\n'
      break
  }

  if (opts.out === '-') {
    process.stdout.write(tsText)
  } else {
    try {
      writeFile(opts.out, tsText)
    } catch (error) {
      console.error(`Failed to write to output file ${opts.out}`)
      console.error(`Confirm path is writeable or pass "--out -" to print to stdout`)
      return 1
    }
  }

  console.log(`Wrote timestamps to ${opts.out}`)

  if (!opts['no-concat-list']) {
    const concatOutput = (
      (defaultOut
        ? (outFromDirectory || 'album')
        : `/path/to/album`)
      + path.extname(files[0]))

    const concatListPath = opts['concat-list'] || `/tmp/combine-album-concat.txt`
    try {
      await writeFile(concatListPath, files.map(file => `file ${shellescape([path.resolve(file)])}`).join('\n') + '\n')
      console.log(`Generated ffmpeg concat list at ${concatListPath}`)
      console.log(`# To concat:`)
      console.log(`ffmpeg -f concat -safe 0 -i ${shellescape([concatListPath])} -c copy ${shellescape([concatOutput])}`)
    } catch (error) {
      console.warn(`Failed to generate ffmpeg concat list`)
      console.warn(error)
    } finally {
      console.log(`(Pass --no-concat-list to skip this step)`)
    }
  }

  return 0
}

main().then(
  code => process.exit(code),
  err => {
    console.error(err)
    process.exit(1)
  })
